經過 Day 25 的 Cognito 用戶池建置,我們已經有了完整的身份認證服務。今天我們要為 Kyo System 部署 AWS WAF (Web Application Firewall) 與 AWS Shield 安全防護。在雲端環境中,應用層攻擊(如 DDoS、SQL Injection、XSS)是主要威脅,我們需要在 CloudFront 和 ALB 層面建立多層防護,確保系統的可用性與安全性。
/**
* AWS 安全防護架構
*
* ┌──────────────────────────────────────────────┐
* │ AWS Security Layer Architecture │
* └──────────────────────────────────────────────┘
*
* Layer 1: AWS Shield (DDoS Protection)
* ┌─────────────────────────────────────┐
* │ ┌──────────┐ ┌──────────┐ │
* │ │ Shield │ │ Shield │ │
* │ │ Standard │ │ Advanced │ │
* │ └──────────┘ └──────────┘ │
* │ • 自動啟用 • $3000/月 │
* │ • L3/L4 DDoS • 進階保護 │
* │ • 免費 • 成本保護 │
* └─────────────────────────────────────┘
* ↓
* Layer 2: AWS WAF (Application Firewall)
* ┌─────────────────────────────────────┐
* │ CloudFront / ALB / API Gateway │
* │ │
* │ ✅ SQL Injection 防護 │
* │ ✅ XSS (Cross-Site Scripting) │
* │ ✅ Rate-based Rules │
* │ ✅ Geo Blocking │
* │ ✅ IP Sets (Whitelist/Blacklist) │
* │ ✅ Bot Control │
* │ ✅ Custom Rules │
* └─────────────────────────────────────┘
* ↓
* Layer 3: Application Security
* ┌─────────────────────────────────────┐
* │ • API Rate Limiting (自建) │
* │ • JWT Validation │
* │ • Input Validation │
* │ • RBAC Authorization │
* └─────────────────────────────────────┘
*
* 定價 (us-east-1):
* WAF:
* - Web ACL: $5.00/month
* - Rules: $1.00/month per rule
* - Requests: $0.60 per 1M requests
* - Bot Control: $10.00/month + $1.00 per 1M requests
*
* Shield Advanced:
* - $3,000/month per organization
* - DDoS Response Team (DRT) 支援
* - 成本保護(DDoS 期間流量費用減免)
* - 進階指標與報告
*
* 建議:
* - 一般 SaaS: WAF + Shield Standard
* - 高可用需求: WAF + Shield Advanced
* - 成本敏感: WAF only
*/
// infrastructure/lib/waf-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import { Construct } from 'constructs';
export interface WAFStackProps extends cdk.StackProps {
scope: 'CLOUDFRONT' | 'REGIONAL';
resourceArn?: string; // ALB/API Gateway ARN (REGIONAL 需要)
}
export class WAFStack extends cdk.Stack {
public readonly webACL: wafv2.CfnWebACL;
constructor(scope: Construct, id: string, props: WAFStackProps) {
super(scope, id, props);
/**
* 建立 IP Sets
*/
// 允許清單(可信任的 IP)
const allowedIPSet = new wafv2.CfnIPSet(this, 'AllowedIPSet', {
name: 'kyo-allowed-ips',
scope: props.scope,
ipAddressVersion: 'IPV4',
addresses: [
// 範例:辦公室 IP
'203.0.113.0/24',
// 範例:CI/CD IP
'198.51.100.0/24',
],
description: 'Trusted IP addresses (office, CI/CD, etc.)',
});
// 封鎖清單(已知惡意 IP)
const blockedIPSet = new wafv2.CfnIPSet(this, 'BlockedIPSet', {
name: 'kyo-blocked-ips',
scope: props.scope,
ipAddressVersion: 'IPV4',
addresses: [
// 動態更新(透過 Lambda 或手動)
],
description: 'Blocked IP addresses',
});
/**
* 建立 Web ACL
*/
this.webACL = new wafv2.CfnWebACL(this, 'KyoWebACL', {
name: 'kyo-web-acl',
scope: props.scope,
defaultAction: { allow: {} }, // 預設允許
// 規則列表(依優先順序執行)
rules: [
// Rule 1: 封鎖清單 IP
{
name: 'BlockKnownBadIPs',
priority: 0,
statement: {
ipSetReferenceStatement: {
arn: blockedIPSet.attrArn,
},
},
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'BlockKnownBadIPs',
},
},
// Rule 2: 允許清單 IP(跳過後續檢查)
{
name: 'AllowTrustedIPs',
priority: 1,
statement: {
ipSetReferenceStatement: {
arn: allowedIPSet.attrArn,
},
},
action: { allow: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AllowTrustedIPs',
},
},
// Rule 3: AWS Managed Rules - Core Rule Set
{
name: 'AWSManagedRulesCommonRuleSet',
priority: 2,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
excludedRules: [
// 排除誤判規則(依需求調整)
// { name: 'SizeRestrictions_BODY' },
],
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesCommonRuleSet',
},
},
// Rule 4: SQL Injection 防護
{
name: 'AWSManagedRulesSQLiRuleSet',
priority: 3,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesSQLiRuleSet',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesSQLiRuleSet',
},
},
// Rule 5: Known Bad Inputs 防護
{
name: 'AWSManagedRulesKnownBadInputsRuleSet',
priority: 4,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesKnownBadInputsRuleSet',
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWSManagedRulesKnownBadInputsRuleSet',
},
},
// Rule 6: 地理封鎖(範例:只允許台灣、日本、美國)
{
name: 'GeoBlocking',
priority: 5,
statement: {
notStatement: {
statement: {
geoMatchStatement: {
countryCodes: ['TW', 'JP', 'US', 'SG'],
},
},
},
},
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'GeoBlocking',
},
},
// Rule 7: Rate-based Rule (API 速率限制)
{
name: 'RateLimitAPI',
priority: 6,
statement: {
rateBasedStatement: {
limit: 2000, // 5 分鐘內 2000 requests
aggregateKeyType: 'IP',
scopeDownStatement: {
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
positionalConstraint: 'STARTS_WITH',
searchString: '/api/',
textTransformations: [
{ priority: 0, type: 'LOWERCASE' },
],
},
},
},
},
action: {
block: {
customResponse: {
responseCode: 429,
customResponseBodyKey: 'rate-limit-exceeded',
},
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'RateLimitAPI',
},
},
// Rule 8: 登入端點特殊保護
{
name: 'LoginEndpointProtection',
priority: 7,
statement: {
rateBasedStatement: {
limit: 100, // 5 分鐘內 100 requests
aggregateKeyType: 'IP',
scopeDownStatement: {
andStatement: {
statements: [
{
byteMatchStatement: {
fieldToMatch: { uriPath: {} },
positionalConstraint: 'EXACTLY',
searchString: '/api/auth/login',
textTransformations: [
{ priority: 0, type: 'LOWERCASE' },
],
},
},
{
byteMatchStatement: {
fieldToMatch: { method: {} },
positionalConstraint: 'EXACTLY',
searchString: 'POST',
textTransformations: [
{ priority: 0, type: 'NONE' },
],
},
},
],
},
},
},
},
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'LoginEndpointProtection',
},
},
// Rule 9: 自訂規則 - 封鎖特定 User-Agent
{
name: 'BlockBadBots',
priority: 8,
statement: {
orStatement: {
statements: [
{
byteMatchStatement: {
fieldToMatch: {
singleHeader: { name: 'user-agent' },
},
positionalConstraint: 'CONTAINS',
searchString: 'badbot',
textTransformations: [
{ priority: 0, type: 'LOWERCASE' },
],
},
},
{
byteMatchStatement: {
fieldToMatch: {
singleHeader: { name: 'user-agent' },
},
positionalConstraint: 'CONTAINS',
searchString: 'scrapy',
textTransformations: [
{ priority: 0, type: 'LOWERCASE' },
],
},
},
],
},
},
action: { block: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'BlockBadBots',
},
},
],
// 自訂回應內容
customResponseBodies: {
'rate-limit-exceeded': {
contentType: 'APPLICATION_JSON',
content: JSON.stringify({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
}),
},
},
// 可見性設定
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'KyoWebACL',
},
});
/**
* 關聯到資源(REGIONAL 需要)
*/
if (props.scope === 'REGIONAL' && props.resourceArn) {
new wafv2.CfnWebACLAssociation(this, 'WebACLAssociation', {
webAclArn: this.webACL.attrArn,
resourceArn: props.resourceArn,
});
}
/**
* CloudWatch Alarms
*/
const topic = new sns.Topic(this, 'WAFAlertTopic', {
displayName: 'WAF Security Alerts',
});
// 告警 1: 封鎖請求過多
const blockedRequestsAlarm = new cloudwatch.Alarm(this, 'BlockedRequestsAlarm', {
alarmName: 'kyo-waf-high-blocked-requests',
metric: new cloudwatch.Metric({
namespace: 'AWS/WAFV2',
metricName: 'BlockedRequests',
dimensionsMap: {
WebACL: this.webACL.name!,
Rule: 'ALL',
Region: this.region,
},
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
threshold: 1000, // 5 分鐘內超過 1000 個封鎖請求
evaluationPeriods: 1,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
blockedRequestsAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(topic));
// 告警 2: Rate Limit 頻繁觸發
const rateLimitAlarm = new cloudwatch.Alarm(this, 'RateLimitAlarm', {
alarmName: 'kyo-waf-rate-limit-triggered',
metric: new cloudwatch.Metric({
namespace: 'AWS/WAFV2',
metricName: 'BlockedRequests',
dimensionsMap: {
WebACL: this.webACL.name!,
Rule: 'RateLimitAPI',
Region: this.region,
},
statistic: 'Sum',
period: cdk.Duration.minutes(5),
}),
threshold: 100,
evaluationPeriods: 2,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
rateLimitAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(topic));
/**
* Outputs
*/
new cdk.CfnOutput(this, 'WebACLArn', {
value: this.webACL.attrArn,
description: 'WAF Web ACL ARN',
});
new cdk.CfnOutput(this, 'WebACLId', {
value: this.webACL.attrId,
description: 'WAF Web ACL ID',
});
}
}
// infrastructure/lib/cloudfront-waf-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
import { WAFStack } from './waf-stack';
export class CloudFrontWAFStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/**
* 建立 WAF (必須在 us-east-1 for CloudFront)
*/
const wafStack = new WAFStack(this, 'CloudFrontWAF', {
scope: 'CLOUDFRONT',
env: { region: 'us-east-1' }, // CloudFront WAF 必須在 us-east-1
});
/**
* S3 Bucket for static assets
*/
const assetsBucket = new s3.Bucket(this, 'AssetsBucket', {
bucketName: `kyo-assets-${this.account}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
/**
* CloudFront Distribution
*/
const distribution = new cloudfront.Distribution(this, 'KyoDistribution', {
comment: 'Kyo System CDN',
// 預設行為:靜態資產
defaultBehavior: {
origin: new origins.S3Origin(assetsBucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
compress: true,
},
// 額外行為:API 請求
additionalBehaviors: {
'/api/*': {
origin: new origins.HttpOrigin('api.kyong.com', {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
httpsPort: 443,
customHeaders: {
'X-Custom-Header': 'kyo-cloudfront',
},
}),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, // API 不快取
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
},
},
// 關聯 WAF
webAclId: wafStack.webACL.attrArn,
// 自訂錯誤回應
errorResponses: [
{
httpStatus: 403,
responseHttpStatus: 403,
responsePagePath: '/error.html',
ttl: cdk.Duration.seconds(10),
},
{
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/404.html',
ttl: cdk.Duration.seconds(300),
},
],
// 地理限制(與 WAF 配合)
geoRestriction: cloudfront.GeoRestriction.allowlist(
'TW', 'JP', 'US', 'SG'
),
// SSL/TLS 憑證
certificate: undefined, // 使用 ACM 憑證
domainNames: ['app.kyong.com', 'www.kyong.com'],
// 預設根物件
defaultRootObject: 'index.html',
// 啟用 IPv6
enableIpv6: true,
// 啟用 logging
enableLogging: true,
logBucket: new s3.Bucket(this, 'CloudFrontLogsBucket', {
bucketName: `kyo-cloudfront-logs-${this.account}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [
{
expiration: cdk.Duration.days(90),
},
],
}),
logFilePrefix: 'cloudfront/',
});
new cdk.CfnOutput(this, 'DistributionDomainName', {
value: distribution.distributionDomainName,
description: 'CloudFront Distribution Domain',
});
}
}
// infrastructure/lib/bot-control-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import { Construct } from 'constructs';
/**
* AWS WAF Bot Control
*
* 功能:
* - 識別並封鎖惡意機器人
* - 允許合法爬蟲(如 Google Bot)
* - CAPTCHA 挑戰可疑流量
*
* 定價:
* - $10/month per Web ACL
* - $1 per 1M requests
*/
export class BotControlStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 建立 CAPTCHA Token Domain
// 需要先在 Route53 註冊
const captchaDomain = 'captcha.kyong.com';
// 建立包含 Bot Control 的 Web ACL
const webACL = new wafv2.CfnWebACL(this, 'BotControlWebACL', {
name: 'kyo-bot-control',
scope: 'REGIONAL',
defaultAction: { allow: {} },
rules: [
// Bot Control Managed Rule
{
name: 'AWSManagedRulesBotControlRuleSet',
priority: 0,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesBotControlRuleSet',
managedRuleGroupConfigs: [
{
awsManagedRulesBotControlRuleSet: {
inspectionLevel: 'COMMON', // COMMON or TARGETED
},
},
],
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'BotControl',
},
},
// CAPTCHA for suspicious traffic
{
name: 'CAPTCHAForSuspiciousTraffic',
priority: 1,
statement: {
rateBasedStatement: {
limit: 500, // 5 分鐘內 500 requests
aggregateKeyType: 'IP',
},
},
action: {
captcha: {
customRequestHandling: {
insertHeaders: [
{
name: 'x-captcha-required',
value: 'true',
},
],
},
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'CAPTCHAChallenge',
},
captchaConfig: {
immunityTimeProperty: {
immunityTime: 300, // 通過後 5 分鐘免驗證
},
},
},
// 允許已知的良好機器人
{
name: 'AllowGoodBots',
priority: 2,
statement: {
orStatement: {
statements: [
// Google Bot
{
byteMatchStatement: {
fieldToMatch: {
singleHeader: { name: 'user-agent' },
},
positionalConstraint: 'CONTAINS',
searchString: 'Googlebot',
textTransformations: [
{ priority: 0, type: 'NONE' },
],
},
},
// Bing Bot
{
byteMatchStatement: {
fieldToMatch: {
singleHeader: { name: 'user-agent' },
},
positionalConstraint: 'CONTAINS',
searchString: 'bingbot',
textTransformations: [
{ priority: 0, type: 'LOWERCASE' },
],
},
},
],
},
},
action: { allow: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AllowGoodBots',
},
},
],
// CAPTCHA 設定
captchaConfig: {
immunityTimeProperty: {
immunityTime: 300, // 預設免疫時間
},
},
// Token Domains (for CAPTCHA)
tokenDomains: [captchaDomain],
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'BotControlWebACL',
},
});
new cdk.CfnOutput(this, 'CAPTCHADomain', {
value: captchaDomain,
description: 'CAPTCHA Token Domain',
});
}
}
// infrastructure/lambda-edge/waf-auto-response.ts
/**
* Lambda@Edge for WAF Auto Response
*
* 功能:
* - 根據威脅等級自動調整 WAF 規則
* - 動態更新 IP 黑名單
* - 異常流量自動封鎖
*/
import { CloudFormationCustomResourceEvent } from 'aws-lambda';
import {
WAFV2Client,
UpdateIPSetCommand,
GetIPSetCommand,
} from '@aws-sdk/client-wafv2';
const wafClient = new WAFV2Client({ region: 'us-east-1' });
const BLOCKED_IP_SET_ID = process.env.BLOCKED_IP_SET_ID!;
const BLOCKED_IP_SET_NAME = process.env.BLOCKED_IP_SET_NAME!;
/**
* CloudWatch Logs 觸發的自動封鎖
*
* 場景:
* 1. 檢測到 DDoS 攻擊
* 2. 短時間內大量 4xx/5xx
* 3. SQL Injection 嘗試
*/
export async function handler(event: any) {
console.log('Event:', JSON.stringify(event, null, 2));
try {
// 解析 CloudWatch Logs
const logEvents = event.awslogs?.data
? JSON.parse(
Buffer.from(event.awslogs.data, 'base64').toString('utf-8')
).logEvents
: [];
// 分析日誌,找出需要封鎖的 IP
const suspiciousIPs = new Set<string>();
for (const logEvent of logEvents) {
const message = JSON.parse(logEvent.message);
// 條件 1: Rate Limit 觸發超過 10 次
if (
message.action === 'BLOCK' &&
message.ruleId === 'RateLimitAPI'
) {
suspiciousIPs.add(message.httpRequest.clientIp);
}
// 條件 2: SQL Injection 嘗試
if (
message.action === 'BLOCK' &&
message.ruleId?.includes('SQLi')
) {
suspiciousIPs.add(message.httpRequest.clientIp);
}
}
if (suspiciousIPs.size === 0) {
console.log('No suspicious IPs found');
return { statusCode: 200, body: 'No action needed' };
}
// 取得現有的 IP Set
const getIPSetResponse = await wafClient.send(
new GetIPSetCommand({
Id: BLOCKED_IP_SET_ID,
Name: BLOCKED_IP_SET_NAME,
Scope: 'CLOUDFRONT',
})
);
const currentAddresses = getIPSetResponse.IPSet?.Addresses || [];
const newAddresses = Array.from(suspiciousIPs).map(ip => `${ip}/32`);
// 合併並去重
const updatedAddresses = Array.from(
new Set([...currentAddresses, ...newAddresses])
);
// 更新 IP Set
await wafClient.send(
new UpdateIPSetCommand({
Id: BLOCKED_IP_SET_ID,
Name: BLOCKED_IP_SET_NAME,
Scope: 'CLOUDFRONT',
Addresses: updatedAddresses,
LockToken: getIPSetResponse.LockToken,
})
);
console.log(`Blocked ${suspiciousIPs.size} new IPs:`, Array.from(suspiciousIPs));
return {
statusCode: 200,
body: JSON.stringify({
blocked: Array.from(suspiciousIPs),
total: updatedAddresses.length,
}),
};
} catch (error) {
console.error('Error:', error);
throw error;
}
}
// infrastructure/lib/waf-logging-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as kinesis from 'aws-cdk-lib/aws-kinesis';
import * as kinesisfirehose from 'aws-cdk-lib/aws-kinesisfirehose';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
/**
* WAF Logging & Analysis Stack
*
* 架構:
* WAF → Kinesis Data Firehose → S3 → Athena
*/
export class WAFLoggingStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/**
* S3 Bucket for WAF logs
*/
const logBucket = new s3.Bucket(this, 'WAFLogsBucket', {
bucketName: `kyo-waf-logs-${this.account}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [
{
transitions: [
{
storageClass: s3.StorageClass.INTELLIGENT_TIERING,
transitionAfter: cdk.Duration.days(30),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
expiration: cdk.Duration.days(365),
},
],
});
/**
* Kinesis Data Firehose
*/
const firehoseRole = new iam.Role(this, 'FirehoseRole', {
assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'),
});
logBucket.grantWrite(firehoseRole);
const deliveryStream = new kinesisfirehose.CfnDeliveryStream(
this,
'WAFLogDeliveryStream',
{
deliveryStreamName: 'aws-waf-logs-kyo',
deliveryStreamType: 'DirectPut',
s3DestinationConfiguration: {
bucketArn: logBucket.bucketArn,
roleArn: firehoseRole.roleArn,
prefix: 'waf-logs/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/',
errorOutputPrefix: 'waf-logs-errors/',
bufferingHints: {
intervalInSeconds: 300, // 5 分鐘
sizeInMBs: 5,
},
compressionFormat: 'GZIP',
},
}
);
/**
* CloudWatch Logs Insights 查詢範例
*/
// 查詢 1: 最常被封鎖的 IP
const query1 = `
fields @timestamp, httpRequest.clientIp, action, terminatingRuleId
| filter action = "BLOCK"
| stats count() as blocked_count by httpRequest.clientIp
| sort blocked_count desc
| limit 20
`;
// 查詢 2: SQL Injection 嘗試
const query2 = `
fields @timestamp, httpRequest.clientIp, httpRequest.uri, terminatingRuleId
| filter terminatingRuleId like /SQLi/
| sort @timestamp desc
`;
// 查詢 3: Rate Limit 觸發統計
const query3 = `
fields @timestamp, httpRequest.clientIp, terminatingRuleId
| filter terminatingRuleId = "RateLimitAPI"
| stats count() as rate_limit_hits by bin(5m)
`;
/**
* CloudWatch Metric Filters
*/
const logGroup = logs.LogGroup.fromLogGroupName(
this,
'WAFLogGroup',
'aws-waf-logs-kyo'
);
// Metric Filter 1: SQL Injection 嘗試
new logs.MetricFilter(this, 'SQLInjectionAttempts', {
logGroup,
filterPattern: logs.FilterPattern.literal(
'[..., rule_id=*SQLi*, ...]'
),
metricNamespace: 'Kyo/WAF',
metricName: 'SQLInjectionAttempts',
metricValue: '1',
defaultValue: 0,
});
// Metric Filter 2: 高頻封鎖 IP
new logs.MetricFilter(this, 'HighBlockRate', {
logGroup,
filterPattern: logs.FilterPattern.literal('[..., action=BLOCK, ...]'),
metricNamespace: 'Kyo/WAF',
metricName: 'BlockedRequests',
metricValue: '1',
defaultValue: 0,
});
new cdk.CfnOutput(this, 'LogBucketName', {
value: logBucket.bucketName,
description: 'WAF Logs S3 Bucket',
});
new cdk.CfnOutput(this, 'FirehoseDeliveryStreamName', {
value: deliveryStream.deliveryStreamName!,
description: 'Kinesis Firehose Delivery Stream',
});
}
}
/**
* AWS WAF 成本優化指南
*
* 定價結構 (us-east-1):
* - Web ACL: $5.00/month
* - Rule: $1.00/month per rule
* - Request: $0.60 per 1M requests
* - Bot Control: $10.00/month + $1.00 per 1M requests
* - CAPTCHA: $0.40 per 1000 challenge attempts
*
* 成本估算範例:
*/
interface WAFCostEstimate {
webACLs: number;
rulesPerACL: number;
monthlyRequests: number; // in millions
botControlEnabled: boolean;
captchaChallenges: number; // in thousands
}
function calculateWAFCost(config: WAFCostEstimate): {
webACLCost: number;
rulesCost: number;
requestsCost: number;
botControlCost: number;
captchaCost: number;
totalMonthlyCost: number;
} {
// Web ACL 費用
const webACLCost = config.webACLs * 5.0;
// Rules 費用
const rulesCost = config.webACLs * config.rulesPerACL * 1.0;
// Requests 費用
const requestsCost = config.monthlyRequests * 0.6;
// Bot Control 費用
const botControlCost = config.botControlEnabled
? 10.0 + config.monthlyRequests * 1.0
: 0;
// CAPTCHA 費用
const captchaCost = config.captchaChallenges * 0.4;
const totalMonthlyCost =
webACLCost + rulesCost + requestsCost + botControlCost + captchaCost;
return {
webACLCost,
rulesCost,
requestsCost,
botControlCost,
captchaCost,
totalMonthlyCost,
};
}
// 範例 1: 小型 SaaS (100M requests/month)
const smallSaaS = calculateWAFCost({
webACLs: 1,
rulesPerACL: 10,
monthlyRequests: 100,
botControlEnabled: false,
captchaChallenges: 0,
});
console.log('=== Small SaaS (100M req/month) ===');
console.log('Web ACL: $' + smallSaaS.webACLCost.toFixed(2));
console.log('Rules: $' + smallSaaS.rulesCost.toFixed(2));
console.log('Requests: $' + smallSaaS.requestsCost.toFixed(2));
console.log('Total: $' + smallSaaS.totalMonthlyCost.toFixed(2));
// 範例 2: 中型 SaaS (1B requests/month, Bot Control)
const mediumSaaS = calculateWAFCost({
webACLs: 2, // CloudFront + ALB
rulesPerACL: 15,
monthlyRequests: 1000,
botControlEnabled: true,
captchaChallenges: 100,
});
console.log('\n=== Medium SaaS (1B req/month) ===');
console.log('Web ACL: $' + mediumSaaS.webACLCost.toFixed(2));
console.log('Rules: $' + mediumSaaS.rulesCost.toFixed(2));
console.log('Requests: $' + mediumSaaS.requestsCost.toFixed(2));
console.log('Bot Control: $' + mediumSaaS.botControlCost.toFixed(2));
console.log('CAPTCHA: $' + mediumSaaS.captchaCost.toFixed(2));
console.log('Total: $' + mediumSaaS.totalMonthlyCost.toFixed(2));
/**
* 成本優化策略:
*
* 1. 規則優化
* ✅ 合併相似規則
* ✅ 移除低效規則
* ✅ 使用 Managed Rules (包含多個規則但只算 1 個)
*
* 2. 流量優化
* ✅ CDN 快取減少回源請求
* ✅ 靜態資源不經過 WAF
* ✅ 健康檢查不計入 WAF
*
* 3. Bot Control
* ✅ 評估是否真的需要
* ✅ 只對關鍵端點啟用
* ✅ 使用 Scope Down Statement 縮小範圍
*
* 4. CAPTCHA
* ✅ 調整觸發閾值
* ✅ 延長免疫時間
* ✅ 只對高風險操作啟用
*
* 5. 監控
* ✅ 設定 Cost Anomaly Detection
* ✅ 定期檢視 Cost Explorer
* ✅ 分析無效規則
*/
我們今天完成了 Kyo System 的 AWS WAF 與 Shield 安全防護:
WAF vs Application Rate Limiting:
Shield Standard vs Advanced:
Bot Control 值得嗎?:
CAPTCHA 最佳實踐: